package com.rao.gmall.item.service.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rao.gmall.common.cache.BloomName;
import com.rao.gmall.common.cache.GmallCache;
import com.rao.gmall.common.constant.RedisConst;
import com.rao.gmall.feign.product.SkuInfoFeignClient;
import com.rao.gmall.feign.search.SkuEsFeignClient;
import com.rao.gmall.item.service.ItemService;
import com.rao.gmall.model.product.BaseCategoryView;
import com.rao.gmall.model.product.SkuInfo;
import com.rao.gmall.model.product.SpuSaleAttr;
import com.sun.scenario.effect.impl.sw.sse.SSEBlend_SRC_OUTPeer;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.checkerframework.checker.units.qual.min;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author nzmxfan
 * @create 2021-08-08-17:10
 */
@Slf4j
@Service
public class ItemServiceImpl implements ItemService {
    /*
     * 1、redisson会死锁吗？
     * 不会
     * 1、redisson 是可重入锁（同一线程内前面加过后面不用加）
     * 2、redisson 会自动解锁吗？
     * 会：看门狗30s。代码闪断，定时线程没有，就不自动续期了，等30s，redis自己删除
     * 默认加锁就是30s
     * 3、推荐不要使用 lock(30,TimeUnit.SECONDS)；失去了自动续期功能、
     * scheduleExpirationRenewal(threadId); 的调用前提 是 if (leaseTime == -1)
     * 没有续期功能;
     * <p>
     * 看门狗为什么？只是为了业务超时，锁时间不够来不停续期的
     * 看门狗还有默认的30秒（lock.lock()）自动过期（redis删），就是防止特殊情况
     * 类似断电 可以正常 释放锁 避免造成死锁吗
     * <p>
     * 看门狗是续期的。每隔10s续满期。
     * <p>
     * <p>
     * <p>
     * 2、redisson分布式业务超时怎么办？
     * 1、看门狗自动续期，每隔1/3看门狗时间就续满看门狗时间
     * <p>
     * <p>
     * <p>
     * 3、数据一致性问题：
     * 数据库修改以后，缓存的数据需要同步过来；【缓存中的所有数据都应当有过期时间】
     * 1）、缓存的每个数据都必须有过期时间？（允许有一段时间数据不一致）【最终一致性】
     * sku:info:50：后台改了50号数据。  30min 分钟过期，
     * 数据过期以后，下一次对此数据的查询会触发更新逻辑。最终缓存中还是新数据。
     * <p>
     * 双写模式：写数据库+写缓存；
     * 本来要同步缓存（改数据的同时给缓存存一份）；
     * 失效模式：写数据库+删缓存；
     * 下一次查询，由于缓存中没有，自己会查数据库，然后又更新到缓存
     * <p>
     * 这个两种。都可以：99% 代码都是运行成功的。只要成功就能改掉数据。
     * 如果失败，就可以寄希望于 30min 分钟过期。
     * <p>
     * 2）、一劳永逸的解决方案
     * 核心：【把数据库的最后一次修改放到缓存】
     * 2.1）、Canal：
     * 安装Canal；开发同步到binlog以后的代码。 sku_info。
     * sku_info_update_5；   拿到5号记录放到缓存
     * 2.2）、mq：
     * 业务操作改了数据库，给mq发消息。
     * id:5   value: xxxx   updateTime: 获取到数据库的修改时间   发给mq
     * 有个线程专门订阅mq消息。  把数据放到缓存中。mq消息乱序
     * 缓存中此记录的时间如果大于我接下要给缓存中放的数据时间。我就放弃此次操作
     * <p>
     * 总结：
     * 面试官：你们怎么用缓存的，什么是缓存一致性问题，你们怎么解决
     * 答：
     * 1）、我们缓存的数据是实时性一致性要求不高（可以容忍一段时间不一致）
     * 2）、缓存一致性问题： 缓存中的数据没有同步到数据库最新的数据
     * 3）、我们业务代码是 失效模式+缓存数据过期时间。
     * 4）、如果失效模式导致了失败与错误数据，会在一段时间后 数据过期，下一次查询触发查询最新的
     * 面试官：怎么保证大并发下，缓存没有，查到的数据也是能放到缓存？怎么解决缓存击穿问题、缓存雪崩问题
     * 1）、加锁；有没有必要用分布式锁？？？？
     * 分布式锁性能太低了。。。
     * 直接操作：4000
     * 缓存没有要争分布式锁：50/s
     * 缓存中有：600/s
     * 最终决定：只加本地锁。
     * <p>
     * <p>
     * 4、锁的粒度问题
     * 1）、锁的粒度越细越快、
     * <p>
     * 5、分布式数据一致性的问题
     * raft： （强一致）【所有的操作都会通过领导】
     * 1,2,3
     * A----1-----把x号记录改成了y----返回成功或者失败
     * 返回成功： 数据库（不是MySQL）集群大多数节点都已经同步了就会给你说成功
     * <p>
     * B----2-----获取x号记录-----领导会告诉还没有记录
     * B----2---获取x记录，领导会返回记录
     * <p>
     * 问题：
     * mysql：后台同步
     * 写： 连上master；
     * master上改的东西一定不是立即读出来
     * 读： 连上slaver
     * 大概在1s左右读来
     * <p>
     * mysql：（【不要纠结，数据要立刻同步？】量子纠缠？）
     * 1、cpu在1ms的时候给mysql提交保存数据
     * 2、   mysql需要3ms才保存完
     * 3、cpu在2ms的时候要mysql此条数据
     * 4、   mysql肯定返回没有
     * <p>
     * 缓存：   失效模式+数据过期+本地锁+？？？
     * 缓存穿透攻击：
     * 查一个不存在的值，缓存没有总查数据库。允许缓存null值。
     * 随机值攻击：（缓存穿透攻击）
     * 1）、/api/item/50   1~10000
     * 2）、/api/item/时间戳变成数字
     * 核心思想： 缓存中有的，基本上就是数据库有的。
     * 布隆过滤器：
     * 1）、准备一个布隆过滤器
     * 2）、每次我们给数据库里面放了什么，告诉布隆
     * 3）、以后查数据
     * 1）、先问布隆过滤器，数据有没有。
     * 1.1）、布隆说有，不一定有。
     * 查缓存，查数据库，缓存null【利用布隆说有的进行null攻击】，请求达不到数据库
     * 1.2）、布隆说没有就一定没有。直接返回null，缓存都不用看
     */

    @Autowired
    SkuInfoFeignClient skuInfoFeignClient;


    @Autowired
    ThreadPoolExecutor executor;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    SkuEsFeignClient skuEsFeignClient;

    // 优化：通过异步编排获取 sku 详情并封装
    /*
     * 1. 自定义业务线程池（创建 ThreadPoolExecutor 对象并返回）
     * 2. 配置线程池参数（application.yml），动态获取属性值
     * 3.
     */

    /**
     * 第四步：根据切面类 GmallCacheAspect 方法上 @Around("@annotation(com.rao.gmall.common.cache.GmallCache)")
     * 指定切入点，并执行切面的环绕通知
     * // 即加了 @GmallCache 的自定义注解的方法将进行环绕通知来拦截目标方法
     *
     * @param skuId
     * @return
     */
    @GmallCache(
//            bloomPrefix = RedisConst.SKUKEY_PREFIX,
//            bloomSuffix = RedisConst.SKUKEY_SUFFIX,
            cacheKeyExpr = RedisConst.SKUKEY_PREFIX + "#{#args[0]}" + RedisConst.SKUKEY_SUFFIX,
            ttl = 60 * 60 * 1000 * 24 * 7,
            bloomName = BloomName.SKU, // 用哪个布隆过滤器可以自己指定
            missDataTtl = 10 * 60 * 1000
    ) // 需要一个切面类，动态切入所有标了这个注解的方法
    @Override
    public Map<String, Object> getSkuInfo(Long skuId) {
        // 查询数据库
        Map<String, Object> fromServiceItemFeign = getFromServiceItemFeign(skuId);

        return fromServiceItemFeign;
    }

    /**
     * 延迟更新（不需要强一致性）
     *
     * @param skuId
     */
    @Override
    public void updateSkuHotScore(Long skuId) {
        log.info("方法执行。。。。。。。。。。。");
        // 每日此点击商品都会更新热度
        CompletableFuture.runAsync(() -> {

            log.info("准备增加热度分。。。。。。。。。。");
            ZSetOperations<String, String> zset = stringRedisTemplate.opsForZSet();
            // 只给 redis 里面加值
            Double hotScore = zset.incrementScore("hotScore", skuId.toString(), 1);
            if (hotScore % 10 == 0) {
                // 点击 10次再向更新热度分
                skuEsFeignClient.updateSkuHotScore(skuId, hotScore.longValue());
            }
        }, executor);

    }

    /**
     * 远程调用 service-product 查询出和当前 skuId 对应的所有信息
     * 缓存的使用逻辑是固定，我们完全可以抽取
     * <p>
     * 就可以使用AOP直接抽取出来,数据改了，缓存也要改？  如何使用SpringCache简化缓存开发
     *
     * @param skuId
     * @return
     */
    private Map<String, Object> getFromServiceItemFeign(Long skuId) {

        // 声明一个 map 用来存储数据并返回
        Map<String, Object> map = new HashMap<>();
        log.info("开始远程查询，远程会操作数据库.....");
        // 查询 suk 详情
        CompletableFuture<SkuInfo> future = CompletableFuture.supplyAsync(() -> {
            SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);
            map.put("skuInfo", skuInfo);
            return skuInfo;
        }, executor);

        // 1-4 交给线程池并行执行
        // 1. 查询分类
        CompletableFuture<Void> future1 = future.thenAcceptAsync((res) -> {
            if (res != null) {
                BaseCategoryView baseCategoryView = skuInfoFeignClient.getBaseCategoryView(res.getCategory3Id());
                map.put("categoryView", baseCategoryView);
            }
        }, executor);

        // 2.sku销售属性相关信息
        CompletableFuture<Void> future2 = future.thenAcceptAsync((res) -> {
            if (res != null) {
                List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, res.getSpuId());
                map.put("spuSaleAttrList", spuSaleAttrListCheckBySku);
            }
        }, executor);

        // 3.sku价格信息
        CompletableFuture<Void> future3 = future.thenAcceptAsync((res) -> {
            if (res != null) {
                BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);
                map.put("price", skuPrice);
            }
        }, executor);

        // 4. spu 下面的所有存在的 sku 组合信息
        CompletableFuture<Void> future4 = future.thenAcceptAsync((res) -> {
            if (res != null) {
                Map<String, Object> skuValueIdsMap = skuInfoFeignClient.getSkuValueIdsMap(res.getSpuId());
                ObjectMapper objectMapper = new ObjectMapper();
                try {
                    String jsonStr = objectMapper.writeValueAsString(skuValueIdsMap);
                    log.info("valuesSkuJson 内容：{}", jsonStr);
                    map.put("valuesSkuJson", jsonStr);
                } catch (JsonProcessingException e) {
                    log.error("商品sku组合数据转换异常：{}", e);
                }
            }
        }, executor);

        // 将几个属性封装
        CompletableFuture<Void> allOf = CompletableFuture.allOf(future, future1, future2, future3, future4);

        try {
            allOf.get();

        } catch (Exception e) {
            log.error("线程池异常：{}", e);
        }

/*

        // RPC 查询  skuDetail
        // 1.Sku基本信息（名字，id，xxx，价格，sku_描述） sku_info
        // 2.Sku图片信息（sku的默认图片[sku_info]，sku_image[一组图片]）
        SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);
        if (skuInfo != null) {
            map.put("skuInfo", skuInfo);
            // 3.Sku分类信息（sku_info[只有三级分类]，根据这个三级分类查出所在的一级，二级分类内容，连上三张分类表继续查）
            BaseCategoryView baseCategoryView = skuInfoFeignClient.getBaseCategoryView(skuInfo.getCategory3Id());
            map.put("categoryView", baseCategoryView);
            // 4.Sku销售属性相关信息（查出自己的sku组合，还要查出这个sku所在的spu定义了的所有销售属性和属性值）
            List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
            map.put("spuSaleAttrList", spuSaleAttrListCheckBySku);
            // 5.Sku价格信息（平台可以单独修改价格，sku后续会放入缓存，为了回显最新价格，所以单独获取）
            BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);
            map.put("price", skuPrice);
            // 6.SPU下面的所有存在的sku组合信息 {"121|123|156":65,"122|123|111":67}
            // 前端这里还需要把map转成json字符串， JSON.parse(.valuesSkuJson)
            Map<String, Object> skuValueIdsMap = skuInfoFeignClient.getSkuValueIdsMap(skuInfo.getSpuId());
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                String jsonStr = objectMapper.writeValueAsString(skuValueIdsMap);
                log.info("valuesSkuJson 内容：{}", jsonStr);
                map.put("valuesSkuJson", jsonStr);
            } catch (JsonProcessingException e) {
                log.error("商品sku组合数据转换异常：{}", e);
            }
        }
*/

        return map;
    }



/*

    // 以为为手写分布式锁的验证过程，可删除



    // 为了避免缓存击穿，引入锁
    // 阶段一： 本地锁无法对分布式架构无法产生作用，故而需要使用分布式锁（所有中间件，此处使用 redis）-----> 加锁、业务逻辑、删锁
    // 阶段二： 避免因程序代码异常或其它因素中断，导致锁一直不释放，故而加入过期时间
    //          stringRedisTemplate.opsForValue().setIfAbsent("lock", "suo", 10, TimeUnit.SECONDS);
    // 阶段三： 验证成功锁为自己的，使用 UUID 随机数进行控制---------> 避免因并行而被抢占锁
    // 阶段四： 删除锁之前，验证成功但还未删除，此时锁过期，其他线程获得锁，被该次操作删除--------->判断与删除不是原子性
    // 阶段五： 通过使用脚本控制判断、删除的原子性 -------------> 解决业务处理时间导致锁存在的时间浪费或锁提前删除复杂程度较高
    // 阶段六（最终阶段）： 使用 redisson 框架 ---------> 操作简洁、功能强大

    @SneakyThrows
    @Override
    public Map<String, Object> getSkuInfo(Long skuId) {
        // 查询缓存
        Map<String, Object> cache = selectCache(skuId);
        if (cache == null) {
            log.info("缓存未命中，准备查询数据库.......");
            // 为锁加入过期时间，避免因程序代码异常或其它因素中断，导致锁一直不释放
            Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "suo", 10, TimeUnit.SECONDS);
            if (lock){
                log.info("获取锁成功，再次查询缓存，无数据则查询数据库并保存到缓存，最后返回数据");
                Map<String, Object> cacheAgain = selectCache(skuId);
                if (cacheAgain == null){
                    Map<String, Object> fromServiceItemFeign;
                    try {
                        fromServiceItemFeign = getFromServiceItemFeign(skuId);
                        saveCache(skuId, fromServiceItemFeign);
                    } finally {
                        // 删除锁（必须执行）
                        stringRedisTemplate.delete("lock");
                    }
                    return fromServiceItemFeign;
                }
               return cacheAgain;
            }else {
                log.info("抢锁失败，继续进行回旋。。。。。。。。。。。");
                while (true){

                    if (stringRedisTemplate.opsForValue().setIfAbsent("lock", "suo")){
                        // 抢锁成功，进行查询
                        getSkuInfo(skuId);
                    }
                }
            }
        }
        log.info("缓存命中，直接返回.....");
        return cache;
    }

    // 查询缓存
    @SneakyThrows
    private Map<String, Object> selectCache(Long skuId) {
        ObjectMapper mapper = new ObjectMapper();
        // 获取 redis 客户端
        ValueOperations<String, String> operation = stringRedisTemplate.opsForValue();
        String redisContent = operation.get("sku:info:" + skuId);
        if (StringUtils.isEmpty(redisContent)) {
            return null;
        } else {
            // redis 服务器中有数据，讲数据返回
            return mapper.readValue(redisContent, new TypeReference<Map<String, Object>>() {
            });
        }
    }

    // 向 redis 中保存数据
    @SneakyThrows
    private void saveCache(Long skuId, Map<String, Object> fromServiceItemFeign) {
        // 获取客户端
        ValueOperations<String, String> operation = stringRedisTemplate.opsForValue();
        // 讲对象转换为字符串
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonStr = objectMapper.writeValueAsString(fromServiceItemFeign);
        // 写入 reids 中
        operation.set("sku:info:" + skuId, jsonStr);
    }

    *//**
     * 远程调用 service-product 查询出和当前 skuId 对应的所有信息
     * 缓存的使用逻辑是固定，我们完全可以抽取
     * <p>
     * 就可以使用AOP直接抽取出来,数据改了，缓存也要改？  如何使用SpringCache简化缓存开发
     *
     * @param skuId
     * @return
     *//*
    private Map<String, Object> getFromServiceItemFeign(Long skuId) {

        log.info("开始远程查询，远程会操作数据库.....");
        // 声明一个 map 用来存储数据并返回
        HashMap<String, Object> map = new HashMap<>();

        // RPC 查询  skuDetail
        // 1.Sku基本信息（名字，id，xxx，价格，sku_描述） sku_info
        // 2.Sku图片信息（sku的默认图片[sku_info]，sku_image[一组图片]）
        SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);
        if (skuInfo != null) {
            map.put("skuInfo", skuInfo);
            // 3.Sku分类信息（sku_info[只有三级分类]，根据这个三级分类查出所在的一级，二级分类内容，连上三张分类表继续查）
            BaseCategoryView baseCategoryView = skuInfoFeignClient.getBaseCategoryView(skuInfo.getCategory3Id());
            map.put("categoryView", baseCategoryView);
            // 4.Sku销售属性相关信息（查出自己的sku组合，还要查出这个sku所在的spu定义了的所有销售属性和属性值）
            List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
            map.put("spuSaleAttrList", spuSaleAttrListCheckBySku);
            // 5.Sku价格信息（平台可以单独修改价格，sku后续会放入缓存，为了回显最新价格，所以单独获取）
            BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);
            map.put("price", skuPrice);
            // 6.SPU下面的所有存在的sku组合信息 {"121|123|156":65,"122|123|111":67}
            // 前端这里还需要把map转成json字符串， JSON.parse(.valuesSkuJson)
            Map<String, Object> skuValueIdsMap = skuInfoFeignClient.getSkuValueIdsMap(skuInfo.getSpuId());
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                String jsonStr = objectMapper.writeValueAsString(skuValueIdsMap);
                log.info("valuesSkuJson 内容：{}", jsonStr);
                map.put("valuesSkuJson", jsonStr);
            } catch (JsonProcessingException e) {
                log.error("商品sku组合数据转换异常：{}", e);
            }
        }
        return map;
    }*/
}
